<?php

/**
 * 
 * @author      : Conveyor Group <steven@conveyorgroup.com>
 * 
 * @package     : Multiple file upload support
 * @description : Extends CodeIgniters native Upload class to add support for multiple uploads.
 * 
 */
if (!defined("BASEPATH")) {
  exit("No direct script access allowed");
}

class MY_Upload extends CI_Upload {

  /**
   * Properties
   */
  protected $_multi_upload_data = array();
  protected $_multi_file_name_override = "";

  /**
   * Initialize preferences
   *
   * @access public
   * @param array
   * @return void
   */
  public function initialize($config = array()) {
    //Upload default settings.
    $defaults = array(
        "max_size" => 0,
        "max_width" => 0,
        "max_height" => 0,
        "max_filename" => 0,
        "allowed_types" => "",
        "file_temp" => "",
        "file_name" => "",
        "orig_name" => "",
        "file_type" => "",
        "file_size" => "",
        "file_ext" => "",
        "upload_path" => "",
        "overwrite" => FALSE,
        "encrypt_name" => FALSE,
        "is_image" => FALSE,
        "image_width" => "",
        "image_height" => "",
        "image_type" => "",
        "image_size_str" => "",
        "error_msg" => array(),
        "mimes" => array(),
        "remove_spaces" => TRUE,
        "xss_clean" => FALSE,
        "temp_prefix" => "temp_file_",
        "client_name" => ""
    );

    //Set each configuration.
    foreach ($defaults as $key => $val) {
      if (isset($config[$key])) {
        $method = "set_{$key}";
        if (method_exists($this, $method)) {
          $this->$method($config[$key]);
        } else {
          $this->$key = $config[$key];
        }
      } else {
        $this->$key = $val;
      }
    }

    //Check if file_name was provided.
    if (!empty($this->file_name)) {
      //Multiple file upload.
      if (is_array($this->file_name)) {
        //Clear file name override.
        $this->_file_name_override = "";

        //Set multiple file name override.
        $this->_multi_file_name_override = $this->file_name;
        //Single file upload.
      } else {
        //Set file name override.
        $this->_file_name_override = $this->file_name;

        //Clear multiple file name override.
        $this->_multi_file_name_override = "";
      }
    }
  }

  /**
   * File MIME Type
   *
   * Detects the (actual) MIME type of the uploaded file, if possible.
   * The input array is expected to be $_FILES[$field].
   *
   * In the case of multiple uploads, a optional second argument may be
   * passed specifying which array element of the $_FILES[$field] array
   * elements should be referenced (name, type, tmp_name, etc).
   *
   * @access protected
   * @param $file array
   * @param $count int
   * @return void
   */
  protected function _file_mime_type($file, $count = 0) {
    //Mutliple file?
    if (is_array($file["name"])) {
      $tmp_name = $file["tmp_name"][$count];
      $type = $file["type"][$count];
      //Single file.
    } else {
      $tmp_name = $file["tmp_name"];
      $type = $file["type"];
    }

    //We'll need this to validate the MIME info string (e.g. text/plain; charset=us-ascii).
    $regexp = "/^([a-z\-]+\/[a-z0-9\-\.\+]+)(;\s.+)?$/";

    /* Fileinfo Extension - most reliable method.
     *
     * Unfortunately, prior to PHP 5.3 - it's only available as a PECL extension and the
     * more convenient FILEINFO_MIME_TYPE flag doesn't exist.
     */
    if (function_exists("finfo_file")) {
      $finfo = finfo_open(FILEINFO_MIME);
      if (is_resource($finfo)) {
        $mime = @finfo_file($finfo, $tmp_name);
        finfo_close($finfo);

        /* According to the comments section of the PHP manual page,
         * it is possible that this function returns an empty string
         * for some files (e.g. if they don't exist in the magic MIME database).
         */
        if (is_string($mime) && preg_match($regexp, $mime, $matches)) {
          $this->file_type = $matches[1];
          return;
        }
      }
    }

    /* This is an ugly hack, but UNIX-type systems provide a "native" way to detect the file type,
     * which is still more secure than depending on the value of $_FILES[$field]['type'], and as it
     * was reported in issue #750 (https://github.com/EllisLab/CodeIgniter/issues/750) - it's better
     * than mime_content_type() as well, hence the attempts to try calling the command line with
     * three different functions.
     *
     * Notes:
     * - the DIRECTORY_SEPARATOR comparison ensures that we're not on a Windows system
     * - many system admins would disable the exec(), shell_exec(), popen() and similar functions
     * due to security concerns, hence the function_exists() checks
     */
    if (DIRECTORY_SEPARATOR !== "\\") {
      $cmd = "file --brief --mime " . escapeshellarg($tmp_name) . " 2>&1";

      if (function_exists("exec")) {
        /* This might look confusing, as $mime is being populated with all of the output when set in the second parameter.
         * However, we only neeed the last line, which is the actual return value of exec(), and as such - it overwrites
         * anything that could already be set for $mime previously. This effectively makes the second parameter a dummy
         * value, which is only put to allow us to get the return status code.
         */
        $mime = @exec($cmd, $mime, $return_status);
        if ($return_status === 0 && is_string($mime) && preg_match($regexp, $mime, $matches)) {
          $this->file_type = $matches[1];
          return;
        }
      }
    }

    if ((bool) @ini_get("safe_mode") === FALSE && function_exists("shell_exec")) {
      $mime = @shell_exec($cmd);
      if (strlen($mime) > 0) {
        $mime = explode("\n", trim($mime));
        if (preg_match($regexp, $mime[(count($mime) - 1)], $matches)) {
          $this->file_type = $matches[1];
          return;
        }
      }
    }

    if (function_exists("popen")) {
      $proc = @popen($cmd, "r");
      if (is_resource($proc)) {
        $mime = @fread($proc, 512);
        @pclose($proc);
        if ($mime !== FALSE) {
          $mime = explode("\n", trim($mime));
          if (preg_match($regexp, $mime[(count($mime) - 1)], $matches)) {
            $this->file_type = $matches[1];
            return;
          }
        }
      }
    }

    //Fall back to the deprecated mime_content_type(), if available (still better than $_FILES[$field]["type"])
    if (function_exists("mime_content_type")) {
      $this->file_type = @mime_content_type($tmp_name);
      //It's possible that mime_content_type() returns FALSE or an empty string.
      if (strlen($this->file_type) > 0) {
        return;
      }
    }

    //If all else fails, use $_FILES default mime type.
    $this->file_type = $type;
  }

  /**
   * Set Multiple Upload Data
   *
   * @access protected
   * @return void
   */
  protected function set_multi_upload_data() {
    $this->_multi_upload_data[] = array(
        "file_name" => $this->file_name,
        "file_type" => $this->file_type,
        "file_path" => $this->upload_path,
        "full_path" => $this->upload_path . $this->file_name,
        "raw_name" => str_replace($this->file_ext, "", $this->file_name),
        "orig_name" => $this->orig_name,
        "client_name" => $this->client_name,
        "file_ext" => $this->file_ext,
        "file_size" => $this->file_size,
        "is_image" => $this->is_image(),
        "image_width" => $this->image_width,
        "image_height" => $this->image_height,
        "image_type" => $this->image_type,
        "image_size_str" => $this->image_size_str
    );
  }

  /**
   * Get Multiple Upload Data
   *
   * @access public
   * @return array
   */
  public function get_multi_upload_data() {
    return $this->_multi_upload_data;
  }

  /**
   * Multile File Upload
   *
   * @access public
   * @param string
   * @return mixed
   */
  public function do_multi_upload($field) {
    //Is $_FILES[$field] set? If not, no reason to continue.
    if (!isset($_FILES[$field])) {
      return false;
    }

    //Is this really a multi upload?
    if (!is_array($_FILES[$field]["name"])) {
      //Fallback to do_upload method.
      return $this->do_upload($field);
    }

    //Is the upload path valid?
    if (!$this->validate_upload_path()) {
      //Errors will already be set by validate_upload_path() so just return FALSE
      return FALSE;
    }

    //Every file will have a separate entry in each of the $_FILES associative array elements (name, type, etc).
    //Loop through $_FILES[$field]["name"] as representative of total number of files. Use count as key in
    //corresponding elements of the $_FILES[$field] elements.
    for ($i = 0; $i < count($_FILES[$field]["name"]); $i++) {
      //Was the file able to be uploaded? If not, determine the reason why.
      if (!is_uploaded_file($_FILES[$field]["tmp_name"][$i])) {
        //Determine error number.
        $error = (!isset($_FILES[$field]["error"][$i])) ? 4 : $_FILES[$field]["error"][$i];

        //Set error.
        switch ($error) {
          //UPLOAD_ERR_INI_SIZE
          case 1:
            $this->set_error("upload_file_exceeds_limit");
            break;
          //UPLOAD_ERR_FORM_SIZE
          case 2:
            $this->set_error("upload_file_exceeds_form_limit");
            break;
          //UPLOAD_ERR_PARTIAL
          case 3:
            $this->set_error("upload_file_partial");
            break;
          //UPLOAD_ERR_NO_FILE
          case 4:
            $this->set_error("upload_no_file_selected");
            break;
          //UPLOAD_ERR_NO_TMP_DIR
          case 6:
            $this->set_error("upload_no_temp_directory");
            break;
          //UPLOAD_ERR_CANT_WRITE
          case 7:
            $this->set_error("upload_unable_to_write_file");
            break;
          //UPLOAD_ERR_EXTENSION
          case 8:
            $this->set_error("upload_stopped_by_extension");
            break;
          default:
            $this->set_error("upload_no_file_selected");
            break;
        }

        //Return failed upload.
        return FALSE;
      }

      //Set current file data as class variables.
      $this->file_temp = $_FILES[$field]["tmp_name"][$i];
      $this->file_size = $_FILES[$field]["size"][$i];
      $this->_file_mime_type($_FILES[$field], $i);
      $this->file_type = preg_replace("/^(.+?);.*$/", "\\1", $this->file_type);
      $this->file_type = strtolower(trim(stripslashes($this->file_type), '"'));
      $this->file_name = $this->_prep_filename($_FILES[$field]["name"][$i]);
      $this->file_ext = $this->get_extension($this->file_name);
      $this->client_name = $this->file_name;

      //Is the file type allowed to be uploaded?
      if (!$this->is_allowed_filetype()) {
        $this->set_error("upload_invalid_filetype");
        return FALSE;
      }

      //If we're overriding, let's now make sure the new name and type is allowed.
      //Check if a filename was supplied for the current file. Otherwise, use it's given name.
      if (!empty($this->_multi_file_name_override[$i])) {
        $this->file_name = $this->_prep_filename($this->_multi_file_name_override[$i]);

        //If no extension was provided in the file_name config item, use the uploaded one.
        if (strpos($this->_multi_file_name_override[$i], ".") === FALSE) {
          $this->file_name .= $this->file_ext;
          //An extension was provided, lets have it!
        } else {
          $this->file_ext = $this->get_extension($this->_multi_file_name_override[$i]);
        }

        if (!$this->is_allowed_filetype(TRUE)) {
          $this->set_error("upload_invalid_filetype");
          return FALSE;
        }
      }

      //Convert the file size to kilobytes.
      if ($this->file_size > 0) {
        $this->file_size = round($this->file_size / 1024, 2);
      }

      //Is the file size within the allowed maximum?
      if (!$this->is_allowed_filesize()) {
        $this->set_error("upload_invalid_filesize");
        return FALSE;
      }

      //Are the image dimensions within the allowed size?
      //Note: This can fail if the server has an open_basdir restriction.
      if (!$this->is_allowed_dimensions()) {
        $this->set_error("upload_invalid_dimensions");
        return FALSE;
      }

      //Sanitize the file name for security.
      $this->file_name = $this->clean_file_name($this->file_name);

      //Truncate the file name if it's too long
      if ($this->max_filename > 0) {
        $this->file_name = $this->limit_filename_length($this->file_name, $this->max_filename);
      }

      //Remove white spaces in the name
      if ($this->remove_spaces == TRUE) {
        $this->file_name = preg_replace("/\s+/", "_", $this->file_name);
      }

      /* Validate the file name
       * This function appends an number onto the end of
       * the file if one with the same name already exists.
       * If it returns false there was a problem.
       */
      $this->orig_name = $this->file_name;
      if ($this->overwrite == FALSE) {
        $this->file_name = $this->set_filename($this->upload_path, $this->file_name);
        if ($this->file_name === FALSE) {
          return FALSE;
        }
      }

      /* Run the file through the XSS hacking filter
       * This helps prevent malicious code from being
       * embedded within a file. Scripts can easily
       * be disguised as images or other file types.
       */
      if ($this->xss_clean) {
        if ($this->do_xss_clean() === FALSE) {
          $this->set_error("upload_unable_to_write_file");
          return FALSE;
        }
      }

      /* Move the file to the final destination
       * To deal with different server configurations
       * we'll attempt to use copy() first. If that fails
       * we'll use move_uploaded_file(). One of the two should
       * reliably work in most environments
       */
      if (!@copy($this->file_temp, $this->upload_path . $this->file_name)) {
        if (!@move_uploaded_file($this->file_temp, $this->upload_path . $this->file_name)) {
          $this->set_error("upload_destination_error");
          return FALSE;
        }
      }

      /* Set the finalized image dimensions
       * This sets the image width/height (assuming the
       * file was an image). We use this information
       * in the "data" function.
       */
      $this->set_image_properties($this->upload_path . $this->file_name);

      //Set current file data to multi_file_upload_data.
      $this->set_multi_upload_data();
    }

    //Return all file upload data.
    return TRUE;
  }

}
